#include "servuno.h"
#include <QDebug>
#include <QtMath>
#include <QTime>
#include <QThread>

static QTime boardTime;
static qreal randomSeed = -1;
using namespace Aurasim;

// Digital I/O
static QScriptValue fpinMode(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)
    if(context->argumentCount() == 2) {
        const int& pin = context->argument(0).toInt32();
        const int& mode = context->argument(1).toInt32();
        if(jservuno::checkRange(pin)) {
            jservuno::board()->setPinMode(pin, mode);
        }
    }
    return QScriptValue::NullValue;
}

static QScriptValue fdigitalWrite(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)
    if(context->argumentCount() == 2) {
        const int& pin = context->argument(0).toInt32();
        const int& val = context->argument(1).toInt32();
        if(jservuno::checkRange(pin)) {
            jservuno::board()->setDigVal(pin, val);
            jservuno::board()->setSignalType(pin, 0);
        }
    }
    return QScriptValue::NullValue;
}

static QScriptValue fdigitalRead(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)
    if(context->argumentCount() == 1) {
        const int& pin = context->argument(0).toInt32();
        if(jservuno::checkRange(pin))
            return jservuno::board()->digVal(pin);
    }
    return QScriptValue::NullValue;
}

// Analog I/O
static QScriptValue fanalogWrite(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)
    if(context->argumentCount() == 2) {
        const int& pin = context->argument(0).toInt32();
        const int& val = context->argument(1).toInt32();
        if(jservuno::checkRange(pin)) {
            jservuno::board()->setAnaVal(pin, val);
            jservuno::board()->setSignalType(pin, 1);
        }
    }
    return QScriptValue::NullValue;
}

static QScriptValue fanalogRead(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)
    if(context->argumentCount() == 1) {
        const int& pin = context->argument(0).toInt32();
        if(jservuno::checkRange(pin))
            return jservuno::board()->anaVal(pin);
    }
    return QScriptValue::NullValue;
}

static QScriptValue fAnalogreference(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)
//    if(context->argumentCount() == 1) {
//        const int& pin = context->argument(0).toInt32();
//        if(jservuno::checkRange(pin))
//            return jservuno::board()->anaVal(pin);
//    }
    return QScriptValue::NullValue;
}

// Math
static QScriptValue fAbs(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)
    if(context->argumentCount() == 1) {
        const qreal& val = context->argument(0).toNumber();
        return qAbs(val);
    }
    return QScriptValue::NullValue;
}

static QScriptValue fConstrain(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)
    if(context->argumentCount() == 3) {
        const qreal& x = context->argument(0).toNumber();
        const qreal& a = context->argument(1).toNumber();
        const qreal& b = context->argument(2).toNumber();
        const qreal min = qMin(a, b);
        const qreal max = qMax(a, b);
        qreal res;
        if (x >= min && x <= max)
            res = x;
        else if (x < min)
            res = min;
        else
            res = max;

        return QScriptValue(res);
    }
    return QScriptValue::NullValue;
}

static QScriptValue fMap(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)
    if(context->argumentCount() == 5) {
        const qreal& x = context->argument(0).toNumber();
        const qreal& a1 = context->argument(0).toNumber();
        const qreal& b1 = context->argument(0).toNumber();
        const qreal& a2 = context->argument(0).toNumber();
        const qreal& b2 = context->argument(0).toNumber();
        qreal min1 = qMin(a1, b1);
        qreal max1 = qMax(a1, b1);
        qreal min2 = qMax(a2, b2);
        qreal max2 = qMax(a2, b2);
        qreal res = ((x - min1) / (max1 - min1)) * (max2 - min2) + min2;
        return QScriptValue(res);
    }
    return QScriptValue::NullValue;
}

static QScriptValue fMax(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)
    if(context->argumentCount() == 2) {
        const qreal& a = context->argument(0).toNumber();
        const qreal& b = context->argument(0).toNumber();
        return QScriptValue(qMax(a, b));
    }
    return QScriptValue::NullValue;
}


static QScriptValue fMin(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)
    if(context->argumentCount() == 2) {
        const qreal& a = context->argument(0).toNumber();
        const qreal& b = context->argument(0).toNumber();
        return QScriptValue(qMin(a, b));
    }
    return QScriptValue::NullValue;
}

static QScriptValue fPow(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)
    if(context->argumentCount() == 2) {
        const qreal& x = context->argument(0).toNumber();
        const qreal& y = context->argument(0).toNumber();
        return QScriptValue(qPow(x, y));
    }
    return QScriptValue::NullValue;
}

static QScriptValue fSq(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)
    if(context->argumentCount() == 1) {
        const qreal& x = context->argument(0).toNumber();
        return QScriptValue(qPow(x, 2));
    }
    return QScriptValue::NullValue;
}

static QScriptValue fSqrt(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)
    if(context->argumentCount() == 1) {
        const qreal& x = context->argument(0).toNumber();
        return QScriptValue(qSqrt(x));
    }
    return QScriptValue::NullValue;
}

// Random Numbers
static QScriptValue fRandom(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)

    if (randomSeed > 0)
        return QScriptValue(randomSeed);

    if(context->argumentCount() == 2) {
        const qreal& min = context->argument(0).toNumber();
        const qreal& max = context->argument(0).toNumber();
        if (min >= max)
            return QScriptValue::NullValue;

        qreal seed = qrand() * 1.0 / RAND_MAX;

        return QScriptValue(min + seed * (max - min));
    }
    if (context->argumentCount() == 1){
        const qreal& max = context->argument(0).toNumber();
        if (max <= 0)
            return QScriptValue::NullValue;
        qreal seed = qrand() * 1.0 / RAND_MAX;
        return QScriptValue(seed * max);
    }
    return QScriptValue::NullValue;
}

static QScriptValue fRandomSeed(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)
    if (context->argumentCount() == 1){
        const qreal& max = context->argument(0).toNumber();
        if (max <= 0)
            randomSeed = -1;
        qreal seed = qrand() * 1.0 / RAND_MAX;
        randomSeed = seed * max;
        return QScriptValue::NullValue;
    }
    return QScriptValue::NullValue;
}

// Time
static QScriptValue fdelay(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)
    if(context->argumentCount() == 1) {
        const int& misec = context->argument(0).toInt32();
        QThread::msleep(misec);
    }
    return QScriptValue::NullValue;
}

static QScriptValue fDelayMicroseconds(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)
    return QScriptValue(boardTime.elapsed());
}

static QScriptValue fMicros(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)
    if(context->argumentCount() == 1) {
        const int& usec = context->argument(0).toInt32();
        QThread::usleep(usec);
    }
    return QScriptValue::NullValue;
}

static QScriptValue fMillis(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)
    if(context->argumentCount() == 1) {
        const int& misec = context->argument(0).toInt32();
        QThread::msleep(misec);
    }
    return QScriptValue::NullValue;
}

// Trigonometry
static QScriptValue fCos(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)
    if(context->argumentCount() == 1) {
        const int& x = context->argument(0).toInt32();
        return QScriptValue(qCos(x));
    }
    return QScriptValue::NullValue;
}

static QScriptValue fSin(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)
    if(context->argumentCount() == 1) {
        const int& x = context->argument(0).toInt32();
        return QScriptValue(qSin(x));
    }
    return QScriptValue::NullValue;
}

static QScriptValue fTan(QScriptContext *context, QScriptEngine *engine) {
    Q_UNUSED(engine)
    if(context->argumentCount() == 1) {
        const int& x = context->argument(0).toInt32();
        return QScriptValue(qTan(x));
    }
    return QScriptValue::NullValue;
}


jservuno::ServUno::ServUno(QObject *parent):
    QObject (parent),
    mEngine(new QScriptEngine),
    mSimThread(nullptr)
{
    // setup js env
    mEngine->globalObject().setProperty("pinMode", mEngine->newFunction(fpinMode));
    mEngine->globalObject().setProperty("digitalRead", mEngine->newFunction(fdigitalRead));
    mEngine->globalObject().setProperty("digitalWrite", mEngine->newFunction(fdigitalWrite));

    mEngine->globalObject().setProperty("ananlogRead", mEngine->newFunction(fanalogRead));
    mEngine->globalObject().setProperty("analogWrite", mEngine->newFunction(fanalogWrite));
    mEngine->globalObject().setProperty("analogReference", mEngine->newFunction(fAnalogreference));

    mEngine->globalObject().setProperty("abs", mEngine->newFunction(fAbs));
    mEngine->globalObject().setProperty("constrain", mEngine->newFunction(fConstrain));
    mEngine->globalObject().setProperty("map", mEngine->newFunction(fMap));
    mEngine->globalObject().setProperty("max", mEngine->newFunction(fMax));
    mEngine->globalObject().setProperty("min", mEngine->newFunction(fMin));
    mEngine->globalObject().setProperty("pow", mEngine->newFunction(fPow));
    mEngine->globalObject().setProperty("sq", mEngine->newFunction(fSq));
    mEngine->globalObject().setProperty("sqrt", mEngine->newFunction(fSqrt));

    mEngine->globalObject().setProperty("cos", mEngine->newFunction(fCos));
    mEngine->globalObject().setProperty("sin", mEngine->newFunction(fSin));
    mEngine->globalObject().setProperty("tan", mEngine->newFunction(fTan));

    mEngine->globalObject().setProperty("delay", mEngine->newFunction(fdelay));
    mEngine->globalObject().setProperty("delayMicroseconds", mEngine->newFunction(fDelayMicroseconds));
    mEngine->globalObject().setProperty("micros", mEngine->newFunction(fMicros));
    mEngine->globalObject().setProperty("millis", mEngine->newFunction(fMillis));

    mEngine->globalObject().setProperty("random", mEngine->newFunction(fRandom));
    mEngine->globalObject().setProperty("randomSeed", mEngine->newFunction(fRandomSeed));


    mEngine->globalObject().setProperty("LOW", QScriptValue(0));
    mEngine->globalObject().setProperty("HIGH", QScriptValue(1));
    mEngine->globalObject().setProperty("INPUT", QScriptValue(0));
    mEngine->globalObject().setProperty("OUTPUT", QScriptValue(1));
    mEngine->globalObject().setProperty("CHANGE", QScriptValue(11));
    mEngine->globalObject().setProperty("RISING", QScriptValue(12));
    mEngine->globalObject().setProperty("FALLING", QScriptValue(13));
    mEngine->globalObject().setProperty("CHANGE", QScriptValue(11));

    for(int i = 0; i < MAX_PINS; i++) {
        mPinModes[i] = 1;
        mAnaVals[i] = 0;
        mDigVals[i] = 0;
        mSignalTypes[i] = 0;
    }

    // defualt js uno code.
    loadSketch(":/js-samples/sketch.js");
}

jservuno::ServUno::~ServUno()
{
    stopSim();
}

void jservuno::ServUno::loadSketch(const QString &file)
{
    QFile f(file);
    if(f.open(QIODevice::ReadOnly)) {
        QTextStream in(&f);
        mSoureCode = in.readAll();
        f.close();
    }

    // add run code
    mSoureCode += "setup();\n";
    mSoureCode += "while(true)"
                  " loop();";
}

void jservuno::ServUno::startSim()
{
    randomSeed = -1;
    mSimThread = QThread::create([=]{
        boardTime.restart();
        mEngine->evaluate(mSoureCode, mSketchFile);
    });
    mSimThread->start();
}

void jservuno::ServUno::stopSim()
{
    // abort js simulation
    mEngine->abortEvaluation();
    mSimThread->wait();
    mSimThread->terminate();
    mSimThread->deleteLater();
    mSimThread = nullptr;
}

int jservuno::ServUno::digVal(const int &pin) const
{
    if(checkRange(pin))
        return mDigVals[pin];
    else
        return -1;
}

int jservuno::ServUno::anaVal(const int &pin) const
{
    if(checkRange(pin))
        return mAnaVals[pin];
    else
        return -1;
}

int jservuno::ServUno::pinMode(const int &pin) const
{
    if(checkRange(pin))
        return mPinModes[pin];
    else
        return -1;
}

int jservuno::ServUno::signalType(const int &pin) const
{
    return mSignalTypes[pin];
}

void jservuno::ServUno::setDigVal(const int &pin, const int &val)
{
    if(checkRange(pin))
        mDigVals[pin] = val;
}

void jservuno::ServUno::setAnaVal(const int &pin, const int &val)
{
    if(checkRange(pin))
        mAnaVals[pin] = val;
}

void jservuno::ServUno::setPinMode(const int &pin, const int &mode)
{
    if(checkRange(pin))
        mPinModes[pin] = mode;
}

void jservuno::ServUno::setSignalType(const int &pin, const int &type)
{
    mSignalTypes[pin] = type;
}

bool jservuno::checkRange(const int& pin)
{
    return pin >= 0 && pin < MAX_PINS;
}

jservuno::ServUno *jservuno::board()
{
    return ServUno::instance();
}
